from khronos.utils import Deque, INF

class SteadyState(object):
    """This object checks when a certain *numerical* indicator reaches a steady state. The 
    concept of steady state here means that the difference between maximum and minimum in the 
    last 'observations' of 'function' cannot exceed the specified 'amplitude'. This class may be 
    used, for instance, to run simulations without a fixed duration, stopping the simulation when 
    a statistical indicator stabilizes, e.g. a queue size, or response time.
    Example:
        class Foo(Process.Atomic):
            @Chain
            def initialize(self):
                mystat = Tally()
                steady = SteadyState(amplitude=0.01, 
                                     observations=100, 
                                     function=mystat.mean)
                while not steady:
                    yield do_stuff()
                    mystat.collect(some_data)
                    steady.collect()
                self.sim.stop()
                
    The stability condition is defined as follows. Every time the collect() method is called, a 
    value is recorded into the object's history, and the stability check is performed. First of 
    all, to pass this check, the history's amplitude (max-min) may not exceed the specified 
    amplitude value (an absolute value). If the amplitude is exceeded, the oldest history value is 
    removed and this condition is rechecked. After the amplitude check is passed, if the history's 
    size is equal to the required number of observations, the indicator is considered stable and 
    collect() will return True (the 'stable' attribute can also be checked)."""
    def __init__(self, amplitude, observations=100, function=None):
        self.amplitude = amplitude
        self.observations = observations
        self.function = function
        self.history = Deque()
        self.min = +INF
        self.max = -INF
        self.stable = False
        
    def __nonzero__(self):
        return self.stable
        
    def collect(self, value=None):
        if self.amplitude < 0.0:
            raise ValueError("amplitude must be non-negative")
        if value is None:
            value = self.function()
        self.history.appendleft(value)
        self.min = min(self.min, value)
        self.max = max(self.max, value)
        while (self.max - self.min) > self.amplitude:
            self.history.pop()
            self.min = min(self.history)
            self.max = max(self.history)
        self.stable = bool(len(self.history) >= self.observations)
        return self.stable
        
    def stability(self):
        return len(self.history) / float(self.observations)
        
    def clear(self):
        self.history = Deque()
        self.min = +INF
        self.max = -INF
        self.stable = False
        
